跳到主要内容

IO 多路复用详解

这里详细展开讲 IO 多路复用的原理,以及 select,poll,epoll 的区别。具体 Linux 的几种 IO 模式看另一篇笔记,这里不要弄混了它们之间的关系。

IO 多路复用只是一种具体的读取策略,它与阻塞 IO和非阻塞 IO 不是一个层面的概念。

I/O 多路复用详解

select,poll,epoll 都是 IO 多路复用的机制。

I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

什么是 select

它的时间复杂度是 O(n)O(n)

Linux 系统的 select 机制中提供了一个名为 select 的系统调用,该函数存在于函数库 sys/select.h 中,用于监视客户端 I/O 接口的状态,其声明如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,
​ fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是 writefds、readfds、和 exceptfds。

调用后 select 函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果到了立即返回设为 null 即可),函数返回。

当 select 函数返回后,可以 通过遍历 fdset,来找到就绪的描述符。

注意:它仅仅知道了,有 I/O 事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以 select 具有 O(n)O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

select 的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

什么是 poll

它的时间复杂度是 O(n)O(n)

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现。

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};

pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select “参数-值” 传递的方式。

同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。 和select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。

注意:poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的

什么是 epoll

注意:这里关于 epoll 的两种工作模式(LT模式、ET模式)省略了,这部分直接看原文吧

它的时间复杂度是 O(1)O(1)

epoll 是在 2.6内核中提出的,是之前的 select 和 poll 的增强版本。

相对于 select 和 poll 来说,epoll 更加灵活,没有描述符限制。

epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

注意:epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以我们说 epoll 实际上是事件驱动(每个事件关联上 fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了 O(1)O(1)

epoll 的优点主要是一下几个方面:

1、监视的描述符数量不受限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。

而 select 的最大缺点也就是进程打开的 fd 是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。

注意:数量限制也可以选择多进程的解决方案(Apache 就是这样实现的),虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。

2、IO 的效率不会随着监视 fd 的数量的增长而下降(时间复杂度是 O(1)O(1))。epoll 不同于 select 和 poll 轮询的方式,而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数。

如何选择:

如果没有大量的 idle -connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,但是当遇到大量的 idle- connection,就会发现 epoll 的效率大大高于 select/poll。

总结它们的异同

select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

从上面看,select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

提示

注意:epoll 跟 select 都能提供多路 I/O 复用的解决方案。在现在的 Linux 内核里有都能够支持,其中 epoll 是 Linux 所特有,而 select 则应该是 POSIX 所规定,一般操作系统均有实现

在 select/poll 中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll 事先通过 epoll_ctl() 来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是 epoll 的魅力所在。

Reference